package controllers.similarity;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import models.EntityContainer;
import models.PropertyModel;
import uk.ac.shef.wit.simmetrics.similaritymetrics.CosineSimilarity;
import uk.ac.shef.wit.simmetrics.similaritymetrics.JaccardSimilarity;
import uk.ac.shef.wit.simmetrics.similaritymetrics.JaroWinkler;
import uk.ac.shef.wit.simmetrics.similaritymetrics.Levenshtein;
import uk.ac.shef.wit.simmetrics.similaritymetrics.OverlapCoefficient;
import controllers.utils.EntityUtil;


public class EntitySimilarity implements Comparable<EntitySimilarity>
{
    private EntityContainer         e1;
    private EntityContainer         e2;
    private Set<PropertyComparison> pcSet;
    private float                   confidence              = 0;
    private float                   nameConf                = 0;
    private float                   propertyConf            = 0;
    private int                     matchedPropertiesNumber = 0;
    private float                   bonus                   = 0;
    private int                     bonusCount              = 0;


    public EntitySimilarity(EntityContainer e1, EntityContainer e2)
    {
        this.e1 = e1;
        this.e2 = e2;
    }


    public void computeConfidence()
    {
        nameConf = compareInstanceName();
        propertyConf = compareProperties();
        confidence = (nameConf + 2 * propertyConf) / 3;

        // penalty / bonus (experimental)
        float penalty_bonus = 0.025f;
        int propertiesThreshold = 3;

        if(matchedPropertiesNumber <= propertiesThreshold) {
            confidence -= penalty_bonus * (propertiesThreshold - matchedPropertiesNumber);
            confidence = Math.max(0, confidence);
        } else if(matchedPropertiesNumber > propertiesThreshold) {
            confidence += penalty_bonus * confidence;
            confidence = Math.min(1.0f, confidence);
        }
    }


    private float compareInstanceName()
    {
        String[] e1_name = e1.getReadableNames();
        String[] e2_name = e2.getReadableNames();

        Float confidence = 0.0f;

        for(String e1S : e1_name) {
            for(String e2S : e2_name) {
                confidence = Math.max(confidence, stringSimilarity(e1S, e2S));
                if(confidence.isNaN()) {
                    confidence = 0.0f;
                }
            }
        }

        return confidence;
    }


    public float compareProperties()
    {
        // TODO
        // check which propertytype (only datatype-property) of e1 are also a
        // part of e2
        // -> e.g. reference instance e1 has x property-types - e2 has y equal
        // propertytypes (compare only localname without uri)
        // -> compare also the literals of the equal property-types
        float threshold = 0.5f, weight2Factor = 0.75f;

        Set<PropertyComparison> similarProperties = new HashSet<>();
        Set<PropertyComparison> similarValues = new HashSet<>();

        for(PropertyModel p1 : e1.getProperties()) {
            String p1Local = EntityUtil.getLocalName(p1.getURI(), true);
            Set<String> p1values = EntityUtil.getLocalNameSet(p1.getValuesByDefault());
            for(PropertyModel p2 : e2.getProperties()) {
                String p2Local = EntityUtil.getLocalName(p2.getURI(), true);
                float similarityProp = stringSimilarity(p1Local, p2Local);
                float similarityVal = stringeSimilaritySet(p1values, EntityUtil.getLocalNameSet(p2.getValuesByDefault()));
                PropertyComparison pc = new PropertyComparison(p1, p2);
                if(similarityProp > threshold) {
                    pc.setPropertySimilarity(similarityProp);
                    similarProperties.add(pc);
                }
                if(similarityVal > threshold) {
                    pc.setValueSimilarity(similarityVal);
                    similarValues.add(pc);
                }
            }
        }

        Set<String> usedProperties = new HashSet<>();
        Set<PropertyComparison> result = new HashSet<>();
        for(PropertyComparison pc : similarProperties) {
            if(usedProperties.contains(pc.getP1().getURI()) || usedProperties.contains(pc.getP2().getURI())) {
                continue;
            }

            Set<String> value1, value2;
            value1 = EntityUtil.getLocalNameSet(pc.getP1().getValuesByDefault());
            value2 = EntityUtil.getLocalNameSet(pc.getP2().getValuesByDefault());

            float confidence = stringeSimilaritySet(value1, value2);

            if(confidence > threshold * weight2Factor) {
                pc.setValueSimilarity(confidence);
                result.add(pc);
                usedProperties.add(pc.getP1().getURI());
                usedProperties.add(pc.getP2().getURI());
                // System.out.println("PROP: " + pc);
            }
        }

        for(PropertyComparison pc : similarValues) {
            if(usedProperties.contains(pc.getP1().getURI()) || usedProperties.contains(pc.getP2().getURI())) {
                continue;
            }
            if(pc.getPropertySimilarity() > 0) {
                // System.out.println("PV: " + pc);
                result.add(pc);
                usedProperties.add(pc.getP1().getURI());
                usedProperties.add(pc.getP2().getURI());
            }

            String value1, value2;
            value1 = EntityUtil.getLocalName(pc.getP1().getURI(), true);
            value2 = EntityUtil.getLocalName(pc.getP2().getURI(), true);

            float confidence = stringSimilarity(value1, value2);

            if(confidence > threshold * weight2Factor) {
                pc.setPropertySimilarity(confidence);
                result.add(pc);
                usedProperties.add(pc.getP1().getURI());
                usedProperties.add(pc.getP2().getURI());
                // System.out.println("VAL: " + pc);
            }
        }

        // System.out.println(System.lineSeparator() + "Result:");
        // for(PropertyComparison pc : result) {
        // System.out.println(pc);
        // }

        // System.out.println(e1.getProperties().size() + " - " +
        // e2.getProperties().size() + " | " + result.size());
        float weight = 0.0f;
        if(result.size() > 0) {
            for(PropertyComparison pc : result) {
                weight += pc.getWeight();
            }
            weight /= result.size();
        }
        matchedPropertiesNumber = result.size();
        pcSet = result;
        return weight;
    }


    public float compareTypes()
    {
        // TODO
        // check which type of e1 is also a type of e2
        // -> e.g. refernece instance e1 has x types (e.g. Person) - e2 has y
        // equal types (compare only localname without uri)

        return 0.0f;
    }


    public float compareRelatedInstances()
    {
        // TODO
        // if e1 and e2 have equal types e.g. person then check if there are
        // also similiar instances on both sides (e.g. dbpedia and freebase)
        // -> e.g. the type is person then check if dbpedia has an instance A of
        // type Person (not e1) and also check if freebase has a instance B of
        // type Person (not e2)
        // -> check the similarity of A and B
        // -> check how much similar instance exist on both sides with the same
        // type
        // -> (very expensive)

        return 0.0f;
    }


    private float stringeSimilaritySet(Set<String> set1, Set<String> set2)
    {
        float similarity = 0;
        for(String s1 : set1) {
            for(String s2 : set2) {
                similarity = Math.max(similarity, stringSimilarity(s1, s2));
                if(similarity == 0.99)
                    return similarity;
            }
        }

        return similarity;
    }


    private float stringSimilarity(String s1, String s2)
    {
        s1 = s1.replaceAll("[^\\p{L}]", " ");
        s2 = s2.replaceAll("[^\\p{L}]", " ");

        // double SIMPLE_THRESHOLD_DEFAULT = 0.867;
        List<StringSimilarity> sim = new LinkedList<>();
        sim.add(new StringSimilarity(new CosineSimilarity(), 1.0f));
        sim.add(new StringSimilarity(new JaccardSimilarity(), 0.6f));
        sim.add(new StringSimilarity(new JaroWinkler(), 0.1f));
        sim.add(new StringSimilarity(new Levenshtein(), 0.4f));
        sim.add(new StringSimilarity(new OverlapCoefficient(), 1.0f));
        // sim.add(new StringSimilary(new SmithWatermanGotoh(), 0.8f)); //
        // slowest algorithm

        float conf = 0;
        float weightSum = 0;

        for(StringSimilarity sm : sim) {
            conf += sm.getSimilarity(s1, s2) * sm.getWeight();
            weightSum += sm.getWeight();
        }

        weightSum = weightSum == 0 ? 1 : weightSum;
        return conf / weightSum;
    }


    public int getMatchedPropertiesNumber()
    {
        return matchedPropertiesNumber;
    }


    public float getConfidence()
    {
        return confidence;
    }


    public Set<PropertyComparison> getSimilarProperties()
    {
        return pcSet;
    }


    public float getConfidenceWithBonus()
    {
        if(bonusCount == 0) { return confidence; }
        float bonusFactor = 0.05f;
        return Math.min(1.0f, confidence + (bonus / bonusCount) * bonusFactor);
    }


    public void addBonus(float bonus)
    {
        bonusCount += 1;
        this.bonus += bonus;
    }


    public EntityContainer getStartInstance()
    {
        return e1;
    }


    public EntityContainer getMatchingCanditate()
    {
        return e2;
    }


    @Override
    public String toString()
    {
        return e1.getLocalName() + " - " + e2.getLocalName() + ": " + confidence;
    }


    @Override
    public int compareTo(EntitySimilarity es)
    {
        if(this.e1.getURI().equals(es.e1.getURI()) && this.e2.getURI().equals(es.e2.getURI())) {
            return 0;
        } else if(this.confidence <= es.confidence) {
            return -1;
        } else {
            return 1;
        }
    }


    @Override
    public boolean equals(Object obj)
    {
        if(obj instanceof EntitySimilarity) { return this.e1.getURI().equals(((EntitySimilarity) obj).e1.getURI()) && this.e2.getURI().equals(((EntitySimilarity) obj).e2.getURI()); }
        return super.equals(obj);
    }

}